﻿namespace Fiddler.AMF
{
    using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
    using System.Xml;

    class AmfDeserializer
    {
        internal static readonly DateTime BaseDate = new DateTime(1970, 1, 1);
        private static List<IAmfObjectBuilder> _builders = new List<IAmfObjectBuilder>();
        private Stream stream;
        private BinaryReader reader;

        private List<Dictionary<string, object>> referencedObjects = new List<Dictionary<string, object>>();

        public AmfDeserializer(Stream input)
            : this(new BinaryReader(input))
        {
        }

        internal AmfDeserializer(BinaryReader reader)
        {
            this.reader = reader;
            this.stream = reader.BaseStream;
        }

        public AmfMessage DeserializeRequest()
        {
            if (this.stream.Length == 0)
            {
                return new AmfMessage();
            }

            AmfMessage message = new AmfMessage();
            message.Version = ReadUInt16();
            int headerCount = ReadUInt16();
            for (int i = 0; i < headerCount; ++i)
            {
                string key = ReadShortString();
                bool required = this.reader.ReadBoolean();
                int length = ReadInt32(); //doesn't seem necessary?
                object data = ReadData(this.reader.ReadByte());
                message.Headers.Add(new AmfHeader(key, required, data));
            }

            int bodyCount = ReadUInt16();
            for (int i = 0; i < bodyCount; ++i)
            {
                string rawMethod = ReadShortString();
                int index = rawMethod.LastIndexOf('.');
                string method = rawMethod.Substring(index + 1);
                string type = rawMethod.Substring(0, index);
                string target = ReadShortString();
                int length = ReadInt32();
                var objectType = this.reader.ReadByte();
                object data = ReadData(objectType);
                message.Bodies.Add(new AmfBody(method, type, target, data));
            }

            return message;
        }

        public AmfMessage DeserializeResponse()
        {
            if (this.stream.Length == 0)
            {
                return new AmfMessage();
            }

            AmfMessage message = new AmfMessage();
            message.Version = ReadUInt16();
            int headerCount = ReadUInt16();
            for (int i = 0; i < headerCount; ++i)
            {
                string key = ReadShortString();
                bool required = this.reader.ReadBoolean();
                int length = ReadInt32(); //doesn't seem necessary?
                object data = this.ReadData();
                message.Headers.Add(new AmfHeader(key, required, data));
            }

            int bodyCount = ReadUInt16();
            for (int i = 0; i < bodyCount; ++i)
            {
                string rawMethod = ReadShortString();
                string target = ReadShortString();
                int length = ReadInt32();
                var objectType = this.reader.ReadByte();
                object data = ReadData(objectType);
                message.Response.Add(new AmfResponse() { ReplyMethod = rawMethod, Data = data });
            }

            return message;
        }

        private object ReadData(byte? type = null)
        {
            if (type == null)
            {
                type = this.reader.ReadByte();
            }

            AmfDataType t = (AmfDataType)type;
            switch (t)
            {
                case AmfDataType.Number:
                    return ReadDouble();
                case AmfDataType.Boolean:
                    return this.reader.ReadBoolean();
                case AmfDataType.String:
                    return ReadShortString();
                case AmfDataType.Array:
                    return ReadArray();
                case AmfDataType.Date:
                    return ReadDate();
                case AmfDataType.LongString:
                    return ReadLongString();
                case AmfDataType.TypedObject:
                    return ReadTypedObject();
                case AmfDataType.MixedArray:
                    return ReadDictionary();
                case AmfDataType.Null:
                case AmfDataType.Undefined:
                case AmfDataType.End:
                    return null;
                case AmfDataType.UntypedObject:
                    return ReadUntypedObject();
                case AmfDataType.Xml:
                    return ReadXmlDocument();
                case AmfDataType.ReferencedObject:
                    return ReadReference();
                case AmfDataType.MovieClip:
                case AmfDataType.TypeAsObject:
                case AmfDataType.Recordset:
                    throw new NotSupportedException();
                case AmfDataType.Amf3:
                    return this.ReadAmf3TypeMarker();
            }

            return null;
        }

        private object ReadAmf3TypeMarker()
        {
            var de = new Amf3Deserializer(this.reader);
            return de.ReadData();
        }

        private Dictionary<string, object> ReadDictionary()
        {
            int size = ReadInt32();
            var hash = new Dictionary<string, object>(size);
            var key = ReadShortString();
            var type = this.reader.ReadByte();
            while (type != (byte)AmfDataType.End)
            {
                hash.Add(key, this.ReadData(type));

                key = this.ReadShortString();
                type = this.reader.ReadByte();
            }

            return hash;
        }

        private DateTime ReadDate()
        {
            double ms = ReadDouble();
            DateTime date = BaseDate.AddMilliseconds(ms);
            var timeZone = ReadUInt16(); //get's the timezone
            return date;
        }
        private double ReadDouble()
        {
            return BitConverter.ToDouble(ReadInternal(8), 0);
        }

        private int ReadInt32()
        {
            return BitConverter.ToInt32(ReadInternal(4), 0);
        }

        private ushort ReadUInt16()
        {
            return BitConverter.ToUInt16(ReadInternal(2), 0);
        }

        private byte[] ReadInternal(int size)
        {
            byte[] buffer = new byte[size];
            for (int i = size - 1; i >= 0; --i)
            {
                buffer[i] = this.reader.ReadByte();
            }

            return buffer;
        }
        private string ReadLongString()
        {
            int length = ReadInt32();
            return this.ReadString(length);
        }

        private string ReadShortString()
        {
            int length = ReadUInt16();
            return this.ReadString(length);
        }

        private byte[] ReadBytes(int length)
        {
            byte[] buffer = new byte[length];
            this.reader.Read(buffer, 0, length);
            return buffer;
        }

        private string ReadString(int length)
        {
            byte[] buffer = new byte[length];
            this.reader.Read(buffer, 0, length);
            return Encoding.UTF8.GetString(buffer);
        }

        private XmlDocument ReadXmlDocument()
        {
            XmlDocument xml = new XmlDocument();
            xml.LoadXml(ReadLongString());
            return xml;
        }

        private object ReadTypedObject()
        {
            string classType = ReadShortString();
            var hash = ReadUntypedObject();
            object typedObject = TryToReconstitute(classType, hash);
            if (typedObject != null)
            {
                return typedObject;
            }

            //only use the flash object type if it wasn't actual typed
            if (!hash.ContainsKey(SmartObject.ObjectTypeIdentifier))
            {
                hash[SmartObject.ObjectTypeIdentifier] = classType;
            }

            return hash;

        }

        private Dictionary<string, object> ReadUntypedObject()
        {
            var hash = new Dictionary<string, object>();
            var key = ReadShortString();
            var type = this.reader.ReadByte();
            while (type != (byte)AmfDataType.End)
            {
                hash.Add(key, this.ReadData(type));

                key = this.ReadShortString();
                type = this.reader.ReadByte();
            }

            this.referencedObjects.Add(hash);
            return hash;
        }

        private Dictionary<string, object> ReadReference()
        {
            var key = this.ReadInt32();
            return this.referencedObjects[key];
        }

        private List<object> ReadArray()
        {
            int size = ReadInt32();
            var arr = new List<object>(size);
            for (int i = 0; i < size; ++i)
            {
                var dataType = this.reader.ReadByte();
                arr.Add(ReadData(dataType));
            }

            return arr;
        }

        private object TryToReconstitute(string typeName, Dictionary<string, object> parameters)
        {
            foreach (IAmfObjectBuilder reconstitutor in _builders)
            {
                if (string.Compare(typeName, reconstitutor.RegisteredName, true) == 0)
                {
                    if (reconstitutor.CheckParameters(parameters))
                    {
                        return reconstitutor.BuildObject(parameters);
                    }
                }
            }
            return null;
        }
    }
}
